// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using Props = SnapObjects.Data.Properties;

namespace SnapObjects.Data
{
    internal class DataModelBinding
    {
        //DynamicModel
        public DataModelBinding(
            IList<DbColumnInfo> columnSchema,
            ITypeMapper mapper)
        {
            var properties = new List<DataPropertyBinding>();
            var modelProperties = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);

            foreach (var column in columnSchema)
            {
                this.ChekcDbColumInfo(column);

                var mapping = mapper.FindMapping(column.DataType, column.TypeName);

                var name = DynamicModelHelp.GetValidPropertyName(column.ColumnName, modelProperties);

                var type = DynamicModelHelp.GetPropertyType(column.DataType);

                modelProperties.Add(name, type);

                IBindableProperty property = new DynamicPropertyMeta(
                                    column.ColumnOrdinal, name, type);

                properties.Add(new DataPropertyBinding(property, column, mapping));
            }

            this.ModelProperties = modelProperties;
            this.Properties = properties;
        }

        //DynamicModel
        public IDictionary<string, Type> ModelProperties { get; }

        //PocoModel
        public DataModelBinding(
            IList<DbColumnInfo> columnSchema,
            IPocoModelMeta modelMeta,
            ITypeMapper mapper)
        {
            this.BindModel(columnSchema, modelMeta, mapper);
        }

        private void BindModel(
            IList<DbColumnInfo> columnSchema,
            IPocoModelMeta modelMeta,
            ITypeMapper mapper)
        {
            var bindings = new List<DataPropertyBinding>();
            var propertiesByName = modelMeta.PropertyMap;
            string tableName = String.Empty;

            IEqualityComparer<string> comparison = StringComparer.OrdinalIgnoreCase;

            //Avoid duplicate key exception
            var columnPropertyMap = new Dictionary<string, IPocoProperty>(comparison);
            var duplicatedColumns = this.GetDuplicateColumns(columnSchema);
            var duplicatedMapped = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            foreach (var propertyPair in propertiesByName)
            {
                var property = propertyPair.Value;

                if (!property.IsBindable)
                {
                    continue;
                }

                var columnName = property.ColumnName ?? property.Name;

                if (property.SqlColumn != null && property.SqlColumn.ColumnAliasDefined)
                {
                    columnName = property.SqlColumn.ColumnAlias;
                }
                else if (property.SqlCompute != null && property.SqlCompute.AliasDefined)
                {
                    columnName = property.SqlCompute.Alias;
                }

                if (!columnPropertyMap.ContainsKey(columnName))
                {
                    columnPropertyMap.Add(columnName, property);
                }
                else
                {
                    duplicatedMapped.Add(columnName);
                }

                if (String.IsNullOrWhiteSpace(tableName) &&
                    property.IsKey &&
                    property.Attribute.SqlColumnDefined &&
                    property.Attribute.SqlColumn.TableAliasDefined)
                {
                    tableName = property.SqlColumn.TableAlias;
                }
            }

            foreach (var column in columnSchema)
            {
                IPocoProperty mappedProperty = null;
                TypeMapping mapping = null;
                DataPropertyBinding binding = null;

                this.ChekcDbColumInfo(column);

                var typeMapping = mapper.FindMapping(column.DataType, column.TypeName);

                //compute column no alias
                if (String.IsNullOrWhiteSpace(column.ColumnName))
                {
                    mappedProperty = propertiesByName.Select(o => o.Value)
                                        .Where(o => o.ColumnOrder == column.ColumnOrdinal ||
                                                            o.Index == column.ColumnOrdinal)
                                        .FirstOrDefault();

                    if (mappedProperty == null || this.PropertyAlreadyMapped(mappedProperty, columnSchema))
                    {
                        continue;
                    }

                    var assignCompatiable = this.AssignCompatiable(mappedProperty, column, typeMapping, mapper);

                    if (!assignCompatiable)
                    {
                        throw new InvalidOperationException(String.Format(
                            Props.Resource.BINDING_DATATYPENOTMATCH_EXCEPTION,
                            column.DataType.Name,
                            mappedProperty.Name,
                            modelMeta.ModelType.Name));
                    }
                }
                else if (duplicatedColumns.Contains(column.ColumnName)) //Duplicate column name
                {
                    mappedProperty = propertiesByName.Select(o => o.Value)
                                                        .Where(o => o.ColumnOrder == column.ColumnOrdinal ||
                                                                        o.Index == column.ColumnOrdinal)
                                                        .FirstOrDefault();

                    if (mappedProperty == null)
                    {
                        continue;
                    }

                    var assignCompatiable = this.AssignCompatiable(mappedProperty, column, typeMapping, mapper);

                    if (!assignCompatiable)
                    {
                        throw new InvalidOperationException(
                            String.Format(Props.Resource.BINDING_DATATYPENOTMATCH_EXCEPTION,
                            column.DataType.Name,
                            mappedProperty.Name,
                            modelMeta.ModelType.Name));
                    }
                }
                else
                {
                    if (columnPropertyMap.TryGetValue(column.ColumnName, out mappedProperty))
                    {
                        //Multiple properties mapped to one column
                        if (duplicatedMapped.Contains(column.ColumnName))
                        {
                            var propertiesMapped = this.GetDuplicateMapped(modelMeta, column.ColumnName, tableName);

                            foreach (var mapped in propertiesMapped)
                            {
                                if (mapped != mappedProperty &&
                                    !bindings.Exists(o => o.Property == mapped))
                                {
                                    mapping = mapper.FindMapping(mapped.DataType, mapped.TypeName);

                                    binding = new DataPropertyBinding(mapped, column, mapping);

                                    bindings.Add(binding);
                                }
                            }
                        }
                    }
                    else
                    {
                        //compute column no alias
                        mappedProperty = propertiesByName
                            .Select(o => o.Value)
                            .Where(o => o.ColumnOrder == column.ColumnOrdinal ||
                                        o.Index == column.ColumnOrdinal)
                            .FirstOrDefault();

                        if (mappedProperty == null || this.PropertyAlreadyMapped(mappedProperty, columnSchema))
                        {
                            continue;
                        }
                    }

                    var assignCompatiable = this.AssignCompatiable(mappedProperty, column, typeMapping, mapper);

                    if (!assignCompatiable)
                    {
                        throw new InvalidOperationException(String.Format(
                            Props.Resource.BINDING_DATATYPENOTCOMPATIBLE_EXCEPTION,
                            column.DataType.Name,
                            mappedProperty.Name,
                            modelMeta.ModelType.Name));
                    }
                }

                mapping = mapper.FindMapping(mappedProperty.DataType, mappedProperty.TypeName);

                if (mapping != null)
                {
                    binding = new DataPropertyBinding(mappedProperty, column, mapping);

                    if (duplicatedMapped.Contains(column.ColumnName))
                    {
                        if (!bindings.Exists(o => o.Property == mappedProperty))
                        {
                            bindings.Add(binding);
                        }
                    }
                    else
                    {
                        bindings.Add(binding);
                    }
                }
                else
                {
                    throw new NotSupportedException(String.Format(
                        Props.Resource.BINDING_DATATYPEUNSUPPORTED_EXCEPTION, mappedProperty.DataType.Name));
                }
            }

            bindings.OrderBy(o => o.DbColumnInfo.ColumnOrdinal).ToList();

            this.Properties = bindings;
        }

        private ISet<string> GetDuplicateColumns(IList<DbColumnInfo> columnSchema)
        {
            var duplicatedColumns = new List<string>();
            var namedColumns = columnSchema.Select(o => o.ColumnName)
                        .Where(o => o != null && o.Trim() != String.Empty);

            if (namedColumns.Count() != namedColumns.Distinct(StringComparer.OrdinalIgnoreCase).Count())
            {
                duplicatedColumns = namedColumns
                    .GroupBy(o => o.ToLower())
                    .Where(o => o.Count() > 1)
                    .Select(o => o.Key.ToLower())
                    .ToList();
            }

            return new HashSet<string>(duplicatedColumns, StringComparer.OrdinalIgnoreCase);
        }

        private IList<IPocoProperty> GetDuplicateMapped(
            IPocoModelMeta modelMeta,
            string columnName,
            string tableName)
        {
            var duplicateMapped = new List<IPocoProperty>();
            var propertiesByName = modelMeta.PropertyMap;

            foreach (var propertyPair in propertiesByName)
            {
                var property = propertyPair.Value;

                if (!property.IsBindable)
                {
                    continue;
                }

                var mappedName = property.ColumnName ??
                    (property.SqlCompute != null && property.SqlCompute.AliasDefined
                    ? property.SqlCompute.Alias : property.Name);

                if (mappedName.Equals(columnName, StringComparison.OrdinalIgnoreCase))
                {
                    if (property.Attribute.SqlColumnDefined &&
                        property.Attribute.SqlColumn.TableAliasDefined &&
                        !property.Attribute.SqlColumn.TableAlias.Equals(
                            tableName, StringComparison.OrdinalIgnoreCase))
                    {
                        continue;
                    }

                    duplicateMapped.Add(property);
                }
            }

            return duplicateMapped;
        }

        private bool PropertyAlreadyMapped(
            IPocoProperty propertyMeta,
            IList<DbColumnInfo> columnSchema)
        {
            if (propertyMeta.IsCompute && !propertyMeta.SqlCompute.AliasDefined)
            {
                return false;
            }

            DbColumnInfo dbColumnInfo = null;

            var comparison = StringComparison.OrdinalIgnoreCase;

            if (propertyMeta.Attribute.SqlColumnDefined &&
                propertyMeta.Attribute.SqlColumn.ColumnAliasDefined)
            {
                dbColumnInfo = columnSchema
                    .Where(o => o.ColumnName.Equals(propertyMeta.ColumnAlias, comparison))
                    .FirstOrDefault();
            }
            else if (propertyMeta.ColumnName != null)
            {
                dbColumnInfo = columnSchema
                    .Where(o => o.ColumnName.Equals(propertyMeta.ColumnName, comparison))
                    .FirstOrDefault();
            }
            else
            {
                dbColumnInfo = columnSchema
                    .Where(o => o.ColumnName.Equals(propertyMeta.Name, comparison))
                    .FirstOrDefault();
            }

            return dbColumnInfo != null;
        }

        public IReadOnlyCollection<DataPropertyBinding> Properties { get; private set; }

        private bool AssignCompatiable(IPocoProperty mappedProperty, DbColumnInfo column, TypeMapping typeMapping, ITypeMapper typeMapper)
        {
            if (mappedProperty.HasConversion)
            {
                var converter = mappedProperty.ValueConverter;

                return converter.CanConvert(column.DataType, mappedProperty.DataType);
            }
            else
            {
                return typeMapping.AssignCompatiable(mappedProperty.DataType);
            }
        }

        private void ChekcDbColumInfo(DbColumnInfo dbColumn)
        {
            if (dbColumn.DataType == null)
            {
                var message = String.Format(
                    Props.Resource.TYPEMAPPING_NOTFOUND_EXCEPTION,
                    dbColumn.TypeName,
                    dbColumn.ColumnName);

                throw new NotSupportedException(message);
            }
        }
    }
}
